/*
 * Copyright 2009-2010 Nanjing RedOrange ltd (http://www.red-orange.cn)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package redora.service;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import redora.api.fetch.Mode;
import redora.api.fetch.Page;
import redora.db.DatabaseFactory;
import redora.db.Statement;
import redora.exceptions.ConnectException;
import redora.exceptions.PagingException;
import redora.exceptions.PersistException;
import redora.exceptions.QueryException;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.logging.Logger;

import static java.util.logging.Level.SEVERE;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Level.INFO;
import static redora.db.DatabaseFactory.getDBProperty;

/**
 * Base class for the java service entry classes providing the database
 * connection management. This class will cache all prepared statements and it
 * will maintain a database connection. With the makePreparedStatement function
 * a hash in the SQL query determines if a prepared statement is already in the
 * cache.
 *
 * @author Nanjing RedOrange (http://www.red-orange.cn)
 */
public abstract class ServiceBase {

    static final transient Logger l = Logger.getLogger("redora.ServiceBase");

    protected Statement st;
    boolean closeStatement = true;
    boolean closed = false;
    final String schema;
    /** If true, the system will return to auto commit when an transaction is ended.*/
    public boolean defaultAutoCommit;

    /** true if you are in a Transaction @see #beginTransaction */
    public boolean inTransaction = false;

    /** Last time in System.currentTime when this service was invoked. */
    public long last;

    protected ServiceBase(@NotNull String schema) throws ConnectException {
        this.schema = schema;
        st = DatabaseFactory.statement(schema);
        last = System.currentTimeMillis();
        defaultAutoCommit = "true".equals(getDBProperty(schema, "autoCommit"));
    }

    public ServiceBase(@NotNull final ServiceBase chain, @NotNull String schema) {
        this.schema = schema;
        st = chain.st;
        closeStatement = false; //chain master handles this
        defaultAutoCommit = false; //By setting this to false, this service will not mangle with the transaction settings
    }

    public void beginTransaction() throws PersistException {
        inTransaction = true;
        try {
            if (defaultAutoCommit) {
                st.con.con.setAutoCommit(false);
            }
        } catch (SQLException e) {
            reset(l);
            throw new PersistException("Failed to start transaction", e);
        }
    }

    public void commit() throws PersistException {
        inTransaction = false;
        try {
            st.con.con.commit();
            if (defaultAutoCommit) {
                st.con.con.setAutoCommit(true);
            }
        } catch (SQLException e) {
            reset(l);
            throw new PersistException("Failed to commit transaction", e);
        }
    }

    public void rollback() throws PersistException {
        inTransaction = false;
        try {
            st.con.con.rollback();
            if (defaultAutoCommit) {
                st.con.con.setAutoCommit(true);
            }
        } catch (SQLException e) {
            reset(l);
            throw new PersistException("Failed to rollback transaction", e);
        }
    }

    /**
     * A 'final' resort method that closes the statement and connection
     * and re-initializes it. Use reset in the catch blocks where you handle
     * exceptions that should not occur. Using reset(0 in these cases makes
     * the application more robust.
     */
    public void reset(Logger parent) {
        l.log(INFO, "Attempting to reset the connection, from {0}", parent.getName());
        inTransaction = false;
        last = System.currentTimeMillis();
        if (closeStatement) {
            st.close();
            try {
                st = DatabaseFactory.statement(schema);
            } catch (ConnectException e) {
                l.log(SEVERE, "Failed to reset", e);
            }
        }
    }

    /**
     * Normally you do not need this use ServiceFactory.close(Service) instead.
     * This method will release any open statement and connection related to this
     * service.
     */
    public void close() {
        inTransaction = false;
        if (closeStatement) {
            st.close();
        }
        closed = true;
    }

    @Override
    public void finalize() throws Throwable {
        if (!closed && closeStatement) {
            l.log(INFO, "You might want to close services yourself, i will do it now for you");
            close();
        }
        super.finalize();
    }

    /**
     * Adds paging info to the given SQL. This means for example the statement
     * 'select * from myTable' will be enhanced to something like:
     * 'select * from myTable limit 20, 30'
     * <br>
     * It will only add it when Page has Mode.Page. If a result size was not established
     * it will use given SQL to count the number of records. So use this after all
     * the query parameters are set.
     *
     * @param sql (Mandatory) The SQL to which you want to add the LIMIT statement
     * @param page (Mandatory) The Page object managing the paging.
     * @throws PagingException This method can run a count query on the DB, when that fails this is the exception.
     * @return An SQL statement suffixed with limit x,y
     */
    @NotNull
    public String preparePage(@NotNull String sql, @NotNull Page page) throws PagingException {
       if (page.getMode() == Mode.Page) {
            if (page.resultCount() == -1) {
                page.initPageMode(getResultSize(sql));
            }

            return new StringBuilder(sql).append(" limit ").append(page.position() * page.pageSize())
                .append(',').append(page.pageSize()).toString();
       }
       return sql;
    }

    public void execute(@NotNull String sql) throws QueryException {
        try {
            st.st.execute(sql);
        } catch (SQLException e) {
            l.log(SEVERE, "Failed to perform query: " + sql, e);
            reset(l);
            throw new QueryException("Failed to perform query: " + sql, e);
        }
    }

    public ResultSet sqlQuery(@NotNull String sql) throws SQLException {
        return st.st.executeQuery(sql);
    }

    /**
     * Looks up the database meta data to determine if the table exists.
     *
     * @param tableName (Mandatory) Any table name you wish to see if it exists.
     * @return True if the table exists.
     * @throws QueryException When something fails in the query for the existence of tableName.
     */
    public boolean tableExists(@NotNull String tableName) throws QueryException {
        ResultSet rs = null;
        try {
            rs = st.con.con.getMetaData().getTables(null, null, tableName, null);
            return rs.next();
        } catch (SQLException e) {
            l.log(SEVERE, "Failed to open meta data to find table " + tableName, e);
            reset(l);
            throw new QueryException("Failed to open meta data to find table " + tableName, e);
        } finally {
            close(rs);
        }
    }

    /**
     * Retrieves the last attribute name on which the unique key is defined.
     *
     * @param tableName (Mandatory) table name
     * @param keyName (Mandatory) unique index name
     * @return null, or the attribute name of the index
     * @throws QueryException When the meta data could not be retrieved.
     */
    @Nullable
    public String uniqueKeyAttribute(@NotNull String tableName, @NotNull String keyName) throws QueryException {
        ResultSet rs = null;
        try {
            rs = st.con.con.getMetaData().getIndexInfo(null, null, tableName, true, true);
            while (rs.next()) {
                if (keyName.equalsIgnoreCase(rs.getString("INDEX_NAME"))) {
                    return rs.getString("COLUMN_NAME");
                    //TODO now it gets the last value: maybe it should get the first.
                }
            }
        } catch (SQLException e) {
            reset(l);
            throw new QueryException("Cannot retrieve Index: " + keyName, e);
        } finally {
            close(rs);
        }
        return null;
    }

    /**
     * Get the total size of the query size for the given SQL.
     *
     * @param sql (Mandatory) The SQL sentence from which you want to determine the # of records it will produce.
     * @return The count(1) result of given query.
     * @throws PagingException When i cannot connect to the DB or the count(1) query fails
     */
    public int getResultSize(@NotNull String sql) throws PagingException {
        String SQL = sql.replace(sql.substring(sql.indexOf("select") + 7, sql.indexOf("from") - 1), "count(1)");
        ResultSet rs = null;
        try {
            rs = st.st.executeQuery(SQL);
            if (rs.next()) {
                return rs.getInt(1);
            }
        } catch (SQLException e) {
            l.log(SEVERE, "Failed to retrieve query size: " + SQL, e);
            reset(l);
            throw new PagingException("Failed to retrieve query size: " + SQL, e);
        } finally {
            close(rs);
        }
        return 0;
    }

    public static void close(ResultSet rs) {
        try {
            if (rs != null) {
                rs.close();
            }
        } catch (SQLException e) {
            l.log(WARNING, "Failed to close result set", e);
        }
    }
}
